/*
 * Copyright 2002-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.expression.spel.ast;

import org.springframework.asm.Label;
import org.springframework.asm.MethodVisitor;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.*;
import org.springframework.expression.spel.*;
import org.springframework.expression.spel.support.ReflectivePropertyAccessor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Represents a simple property or field reference.
 *
 * @author Andy Clement
 * @author Juergen Hoeller
 * @author Clark Duplichien
 * @since 3.0
 */
public class PropertyOrFieldReference extends SpelNodeImpl {

    private final boolean nullSafe;

    private final String name;

    @Nullable
    private String originalPrimitiveExitTypeDescriptor;

    @Nullable
    private volatile PropertyAccessor cachedReadAccessor;

    @Nullable
    private volatile PropertyAccessor cachedWriteAccessor;


    public PropertyOrFieldReference(boolean nullSafe, String propertyOrFieldName, int startPos, int endPos) {
        super(startPos, endPos);
        this.nullSafe = nullSafe;
        this.name = propertyOrFieldName;
    }


    public boolean isNullSafe() {
        return this.nullSafe;
    }

    public String getName() {
        return this.name;
    }


    @Override
    public ValueRef getValueRef(ExpressionState state) throws EvaluationException {
        return new AccessorLValue(this, state.getActiveContextObject(), state.getEvaluationContext(),
                state.getConfiguration().isAutoGrowNullReferences());
    }

    @Override
    public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
        TypedValue tv = getValueInternal(state.getActiveContextObject(), state.getEvaluationContext(),
                state.getConfiguration().isAutoGrowNullReferences());
        PropertyAccessor accessorToUse = this.cachedReadAccessor;
        if (accessorToUse instanceof CompilablePropertyAccessor) {
            CompilablePropertyAccessor accessor = (CompilablePropertyAccessor) accessorToUse;
            setExitTypeDescriptor(CodeFlow.toDescriptor(accessor.getPropertyType()));
        }
        return tv;
    }

    private TypedValue getValueInternal(TypedValue contextObject, EvaluationContext evalContext,
                                        boolean isAutoGrowNullReferences) throws EvaluationException {

        TypedValue result = readProperty(contextObject, evalContext, this.name);

        // Dynamically create the objects if the user has requested that optional behavior
        if (result.getValue() == null && isAutoGrowNullReferences &&
                nextChildIs(Indexer.class, PropertyOrFieldReference.class)) {
            TypeDescriptor resultDescriptor = result.getTypeDescriptor();
            Assert.state(resultDescriptor != null, "No result type");
            // Create a new collection or map ready for the indexer
            if (List.class == resultDescriptor.getType()) {
                if (isWritableProperty(this.name, contextObject, evalContext)) {
                    List<?> newList = new ArrayList<>();
                    writeProperty(contextObject, evalContext, this.name, newList);
                    result = readProperty(contextObject, evalContext, this.name);
                }
            } else if (Map.class == resultDescriptor.getType()) {
                if (isWritableProperty(this.name, contextObject, evalContext)) {
                    Map<?, ?> newMap = new HashMap<>();
                    writeProperty(contextObject, evalContext, this.name, newMap);
                    result = readProperty(contextObject, evalContext, this.name);
                }
            } else {
                // 'simple' object
                try {
                    if (isWritableProperty(this.name, contextObject, evalContext)) {
                        Class<?> clazz = result.getTypeDescriptor().getType();
                        Object newObject = ReflectionUtils.accessibleConstructor(clazz).newInstance();
                        writeProperty(contextObject, evalContext, this.name, newObject);
                        result = readProperty(contextObject, evalContext, this.name);
                    }
                } catch (InvocationTargetException ex) {
                    throw new SpelEvaluationException(getStartPosition(), ex.getTargetException(),
                            SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, result.getTypeDescriptor().getType());
                } catch (Throwable ex) {
                    throw new SpelEvaluationException(getStartPosition(), ex,
                            SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, result.getTypeDescriptor().getType());
                }
            }
        }
        return result;
    }

    @Override
    public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException {
        writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, newValue);
    }

    @Override
    public boolean isWritable(ExpressionState state) throws EvaluationException {
        return isWritableProperty(this.name, state.getActiveContextObject(), state.getEvaluationContext());
    }

    @Override
    public String toStringAST() {
        return this.name;
    }

    /**
     * Attempt to read the named property from the current context object.
     * @return the value of the property
     * @throws EvaluationException if any problem accessing the property or it cannot be found
     */
    private TypedValue readProperty(TypedValue contextObject, EvaluationContext evalContext, String name)
            throws EvaluationException {

        Object targetObject = contextObject.getValue();
        if (targetObject == null && this.nullSafe) {
            return TypedValue.NULL;
        }

        // 缓存机制
        PropertyAccessor accessorToUse = this.cachedReadAccessor;
        if (accessorToUse != null) {
            if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
                try {
                    return accessorToUse.read(evalContext, contextObject.getValue(), name);
                } catch (Exception ex) {
                    // This is OK - it may have gone stale due to a class change,
                    // let's try to get a new one and call it before giving up...
                }
            }
            this.cachedReadAccessor = null;
        }

        /**
         * 拿到符合条件的 PropertyAccessor
         * */
        List<PropertyAccessor> accessorsToTry =
                getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors());
        // Go through the accessors that may be able to resolve it. If they are a cacheable accessor then
        // get the accessor and use it. If they are not cacheable but report they can read the property
        // then ask them to read it
        try {
            for (PropertyAccessor accessor : accessorsToTry) {
                /**
                 * 判断可以读取
                 * {@link org.springframework.context.expression.BeanExpressionContextAccessor#canRead(EvaluationContext, Object, String)}
                 *  会判断容器中是否有这个 value 这个bean
                 * */
                if (accessor.canRead(evalContext, contextObject.getValue(), name)) {
                    if (accessor instanceof ReflectivePropertyAccessor) {
                        accessor = ((ReflectivePropertyAccessor) accessor).createOptimalAccessor(
                                evalContext, contextObject.getValue(), name);
                    }
                    this.cachedReadAccessor = accessor;
                    /**
                     * 返回读取的结果
                     * {@link org.springframework.context.expression.BeanExpressionContextAccessor#read(EvaluationContext, Object, String)}
                     *      `this.beanFactory.getBean(key);`
                     * */
                    return accessor.read(evalContext, contextObject.getValue(), name);
                }
            }
        } catch (Exception ex) {
            throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_DURING_PROPERTY_READ, name, ex.getMessage());
        }

        if (contextObject.getValue() == null) {
            throw new SpelEvaluationException(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL, name);
        } else {
            throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, name,
                    FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue())));
        }
    }

    private void writeProperty(
            TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue)
            throws EvaluationException {

        if (contextObject.getValue() == null && this.nullSafe) {
            return;
        }
        if (contextObject.getValue() == null) {
            throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL, name);
        }

        PropertyAccessor accessorToUse = this.cachedWriteAccessor;
        if (accessorToUse != null) {
            if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
                try {
                    accessorToUse.write(evalContext, contextObject.getValue(), name, newValue);
                    return;
                } catch (Exception ex) {
                    // This is OK - it may have gone stale due to a class change,
                    // let's try to get a new one and call it before giving up...
                }
            }
            this.cachedWriteAccessor = null;
        }

        List<PropertyAccessor> accessorsToTry =
                getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors());
        try {
            for (PropertyAccessor accessor : accessorsToTry) {
                if (accessor.canWrite(evalContext, contextObject.getValue(), name)) {
                    this.cachedWriteAccessor = accessor;
                    accessor.write(evalContext, contextObject.getValue(), name, newValue);
                    return;
                }
            }
        } catch (AccessException ex) {
            throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE,
                    name, ex.getMessage());
        }

        throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE, name,
                FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue())));
    }

    public boolean isWritableProperty(String name, TypedValue contextObject, EvaluationContext evalContext)
            throws EvaluationException {

        Object value = contextObject.getValue();
        if (value != null) {
            List<PropertyAccessor> accessorsToTry =
                    getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors());
            for (PropertyAccessor accessor : accessorsToTry) {
                try {
                    if (accessor.canWrite(evalContext, value, name)) {
                        return true;
                    }
                } catch (AccessException ex) {
                    // let others try
                }
            }
        }
        return false;
    }

    /**
     * Determines the set of property resolvers that should be used to try and access a property
     * on the specified target type. The resolvers are considered to be in an ordered list,
     * however in the returned list any that are exact matches for the input target type (as
     * opposed to 'general' resolvers that could work for any type) are placed at the start of the
     * list. In addition, there are specific resolvers that exactly name the class in question
     * and resolvers that name a specific class but it is a supertype of the class we have.
     * These are put at the end of the specific resolvers set and will be tried after exactly
     * matching accessors but before generic accessors.
     * @param contextObject the object upon which property access is being attempted
     * @return a list of resolvers that should be tried in order to access the property
     */
    private List<PropertyAccessor> getPropertyAccessorsToTry(
            @Nullable Object contextObject, List<PropertyAccessor> propertyAccessors) {

        Class<?> targetType = (contextObject != null ? contextObject.getClass() : null);

        List<PropertyAccessor> specificAccessors = new ArrayList<>();
        List<PropertyAccessor> generalAccessors = new ArrayList<>();
        for (PropertyAccessor resolver : propertyAccessors) {
            Class<?>[] targets = resolver.getSpecificTargetClasses();
            if (targets == null) {
                // generic resolver that says it can be used for any type
                generalAccessors.add(resolver);
            } else if (targetType != null) {
                for (Class<?> clazz : targets) {
                    if (clazz == targetType) {
                        specificAccessors.add(resolver);
                        break;
                    } else if (clazz.isAssignableFrom(targetType)) {
                        generalAccessors.add(resolver);
                    }
                }
            }
        }
        List<PropertyAccessor> resolvers = new ArrayList<>(specificAccessors);
        generalAccessors.removeAll(specificAccessors);
        resolvers.addAll(generalAccessors);
        return resolvers;
    }

    @Override
    public boolean isCompilable() {
        PropertyAccessor accessorToUse = this.cachedReadAccessor;
        return (accessorToUse instanceof CompilablePropertyAccessor &&
                ((CompilablePropertyAccessor) accessorToUse).isCompilable());
    }

    @Override
    public void generateCode(MethodVisitor mv, CodeFlow cf) {
        PropertyAccessor accessorToUse = this.cachedReadAccessor;
        if (!(accessorToUse instanceof CompilablePropertyAccessor)) {
            throw new IllegalStateException("Property accessor is not compilable: " + accessorToUse);
        }

        Label skipIfNull = null;
        if (this.nullSafe) {
            mv.visitInsn(DUP);
            skipIfNull = new Label();
            Label continueLabel = new Label();
            mv.visitJumpInsn(IFNONNULL, continueLabel);
            CodeFlow.insertCheckCast(mv, this.exitTypeDescriptor);
            mv.visitJumpInsn(GOTO, skipIfNull);
            mv.visitLabel(continueLabel);
        }

        ((CompilablePropertyAccessor) accessorToUse).generateCode(this.name, mv, cf);
        cf.pushDescriptor(this.exitTypeDescriptor);

        if (this.originalPrimitiveExitTypeDescriptor != null) {
            // The output of the accessor is a primitive but from the block above it might be null,
            // so to have a common stack element type at skipIfNull target it is necessary
            // to box the primitive
            CodeFlow.insertBoxIfNecessary(mv, this.originalPrimitiveExitTypeDescriptor);
        }
        if (skipIfNull != null) {
            mv.visitLabel(skipIfNull);
        }
    }

    void setExitTypeDescriptor(String descriptor) {
        // If this property or field access would return a primitive - and yet
        // it is also marked null safe - then the exit type descriptor must be
        // promoted to the box type to allow a null value to be passed on
        if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) {
            this.originalPrimitiveExitTypeDescriptor = descriptor;
            this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor);
        } else {
            this.exitTypeDescriptor = descriptor;
        }
    }


    private static class AccessorLValue implements ValueRef {

        private final PropertyOrFieldReference ref;

        private final TypedValue contextObject;

        private final EvaluationContext evalContext;

        private final boolean autoGrowNullReferences;

        public AccessorLValue(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject,
                              EvaluationContext evalContext, boolean autoGrowNullReferences) {

            this.ref = propertyOrFieldReference;
            this.contextObject = activeContextObject;
            this.evalContext = evalContext;
            this.autoGrowNullReferences = autoGrowNullReferences;
        }

        @Override
        public TypedValue getValue() {
            TypedValue value =
                    this.ref.getValueInternal(this.contextObject, this.evalContext, this.autoGrowNullReferences);
            PropertyAccessor accessorToUse = this.ref.cachedReadAccessor;
            if (accessorToUse instanceof CompilablePropertyAccessor) {
                this.ref.setExitTypeDescriptor(CodeFlow.toDescriptor(((CompilablePropertyAccessor) accessorToUse).getPropertyType()));
            }
            return value;
        }

        @Override
        public void setValue(@Nullable Object newValue) {
            this.ref.writeProperty(this.contextObject, this.evalContext, this.ref.name, newValue);
        }

        @Override
        public boolean isWritable() {
            return this.ref.isWritableProperty(this.ref.name, this.contextObject, this.evalContext);
        }
    }

}
